/* Copyright 2012 William Woodall and John Harrison
 *
 * Additional Contributors: Christopher Baker @bakercp
 */

#if !defined(_WIN32)

#include <stdio.h>
#include <string.h>
#include <sstream>
#include <unistd.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <sys/signal.h>
#include <errno.h>
#include <paths.h>
#include <sysexits.h>
#include <termios.h>
#include <sys/param.h>
#include <pthread.h>

#if defined(__linux__)
#include <linux/serial.h>
#include <sys/file.h>
#endif

#include <sys/select.h>
#include <sys/poll.h>
#include <sys/time.h>
#include <time.h>
// #include "libs/profiling.c"
#ifdef __MACH__
#include <AvailabilityMacros.h>
#include <mach/clock.h>
#include <mach/mach.h>
#endif

#include "serial/impl/unix.h"

#include <libs/strtk.hpp>

#ifndef TIOCINQ
#ifdef FIONREAD
#define TIOCINQ FIONREAD
#else
#define TIOCINQ 0x541B
#endif
#endif

#if defined(MAC_OS_X_VERSION_10_3) && (MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_3)
#include <IOKit/serial/ioss.h>
#endif

#define FTDI_LATENCY_TIMER_PATH "/sys/bus/usb-serial/devices/"

using serial::IOException;
using serial::MillisecondTimer;
using serial::PortNotOpenedException;
using serial::Serial;
using serial::SerialException;
using std::invalid_argument;
using std::string;
using std::stringstream;

MillisecondTimer::MillisecondTimer(const uint32_t millis)
    : expiry(timespec_now())
{
  int64_t tv_nsec = expiry.tv_nsec + (millis * 1e6);
  if (tv_nsec >= 1e9)
  {
    int64_t sec_diff = tv_nsec / static_cast<int>(1e9);
    expiry.tv_nsec = tv_nsec % static_cast<int>(1e9);
    expiry.tv_sec += sec_diff;
  }
  else
  {
    expiry.tv_nsec = tv_nsec;
  }
}

int64_t
MillisecondTimer::remaining()
{
  timespec now(timespec_now());
  int64_t millis = (expiry.tv_sec - now.tv_sec) * 1e3;
  millis += (expiry.tv_nsec - now.tv_nsec) / 1e6;
  return millis;
}

timespec
MillisecondTimer::timespec_now()
{
  timespec time;
#ifdef __MACH__ // OS X does not have clock_gettime, use clock_get_time
  clock_serv_t cclock;
  mach_timespec_t mts;
  host_get_clock_service(mach_host_self(), SYSTEM_CLOCK, &cclock);
  clock_get_time(cclock, &mts);
  mach_port_deallocate(mach_task_self(), cclock);
  time.tv_sec = mts.tv_sec;
  time.tv_nsec = mts.tv_nsec;
#else
  clock_gettime(CLOCK_MONOTONIC, &time);
#endif
  return time;
}

timespec
timespec_from_ms(const uint32_t millis)
{
  timespec time;
  time.tv_sec = millis / 1e3;
  time.tv_nsec = (millis - (time.tv_sec * 1e3)) * 1e6;
  return time;
}

Serial::SerialImpl::SerialImpl(const string &port, unsigned long baudrate,
                               bytesize_t bytesize,
                               parity_t parity, stopbits_t stopbits,
                               flowcontrol_t flowcontrol, bool realtime, bool low_latency)
    : port_(port), fd_(-1), is_open_(false), xonxoff_(false), rtscts_(false),
      baudrate_(baudrate), parity_(parity),
      bytesize_(bytesize), stopbits_(stopbits), flowcontrol_(flowcontrol),
      realtime_(realtime), low_latency_(low_latency)
{
  pthread_mutex_init(&this->read_mutex, NULL);
  pthread_mutex_init(&this->write_mutex, NULL);
  if (port_.empty() == false)
    open();
}

Serial::SerialImpl::~SerialImpl()
{
  close();
  pthread_mutex_destroy(&this->read_mutex);
  pthread_mutex_destroy(&this->write_mutex);
}

bool Serial::SerialImpl::has_lowlatency()
{
  struct serial_struct serial;
  ioctl(fd_, TIOCGSERIAL, &serial);
  return (serial.flags & ASYNC_LOW_LATENCY);
}

void Serial::SerialImpl::open()
{
  if (port_.empty())
  {
    throw invalid_argument("Empty port is invalid.");
  }
  if (is_open_ == true)
  {
    throw SerialException("Serial port already open.");
  }

  fd_ = ::open(port_.c_str(), O_RDWR | O_NOCTTY);

  if (fd_ == -1)
  {
    switch (errno)
    {
    case EINTR:
      // Recurse because this is a recoverable error.
      open();
      return;
    case ENFILE:
    case EMFILE:
      THROW(IOException, "Too many file handles open.");
    default:
      is_open_ = false;
      THROW(IOException, errno);
    }
  }

  ::fcntl(fd_, F_SETFD, FD_CLOEXEC); // Close on exec

  // Make it exclusive
  if (::flock(fd_, LOCK_EX | LOCK_NB) != 0)
    THROW(IOException, "Port already open.");

  ::ioctl(fd_, TIOCEXCL);

  // Reduce serial port latency
  if (low_latency_)
  {
    // Check for ftdi_dio device and write to latency_timer
    std::vector<std::string> str_list;
    strtk::parse(port_, "/", str_list);
    string lt_path = FTDI_LATENCY_TIMER_PATH + str_list.back() + "/latency_timer";
    int lt = ::open(lt_path.c_str(), O_RDWR);
    if (lt >= 0)
    {
      uint8_t changed = 0;
      char latency_string[4];
      ::read(lt, &latency_string, 2);
      int latency_ms = atoi(latency_string);

      if (latency_ms > 1)
      {
        latency_string[0] = '0';
        ::write(lt, &latency_string, 1);
        changed = 1;
        usleep(1000000);
        ::close(lt);
        lt = ::open(lt_path.c_str(), O_RDWR);
      }

      ::read(lt, &latency_string, 2);
      latency_ms = atoi(latency_string);

      if (latency_ms > 1)
      {
        latency_ms = 1;
        latency_string[0] = '1';
        ::write(lt, &latency_string, 1);
        changed = 1;
        usleep(1000000);
      }
      if (changed)
      {
        if (latency_ms == 0)
          printf("UART Latency reduced to 125 us\n");
        else
          printf("UART Latency reduced to %d ms\n", latency_ms);
      }

      ::close(lt);
    }
    else
    {
      // Set realtime
      struct serial_struct serial;
      ioctl(fd_, TIOCGSERIAL, &serial);
      bool has_lowlatency = (serial.flags & ASYNC_LOW_LATENCY);
      serial.flags |= ASYNC_LOW_LATENCY;
      ioctl(fd_, TIOCSSERIAL, &serial);
      if (!has_lowlatency)
      {
        printf("Setting UART flag ASYNC_LOW_LATENCY\n");
        changed_to_lowlatency = true;
        // Wait 1 second for changes to be saved
        usleep(1000000);
      }
      else
      {
        changed_to_lowlatency = false;
      }
    }
  }

  reconfigurePort();
  is_open_ = true;
}

void Serial::SerialImpl::reconfigurePort()
{
  if (fd_ == -1)
  {
    // Can only operate on a valid file descriptor
    THROW(IOException, "Invalid file descriptor, is the serial port open?");
  }

  ::fcntl(fd_, F_SETFD, FD_CLOEXEC); // Close on exec

  struct termios options; // The options for the file descriptor

  if (tcgetattr(fd_, &options) == -1)
  {
    THROW(IOException, "::tcgetattr");
  }

  // set up raw mode / no echo / binary
  options.c_cflag |= (tcflag_t)(CLOCAL | CREAD);
  options.c_lflag &= (tcflag_t) ~(ICANON | ECHO | ECHOE | ECHOK | ECHONL |
                                  ISIG | IEXTEN); //|ECHOPRT

  options.c_oflag &= (tcflag_t) ~(OPOST);
  options.c_iflag &= (tcflag_t) ~(INLCR | IGNCR | ICRNL | IGNBRK);
#ifdef IUCLC
  options.c_iflag &= (tcflag_t)~IUCLC;
#endif
#ifdef PARMRK
  options.c_iflag &= (tcflag_t)~PARMRK;
#endif

  cfmakeraw(&options); // make raw
  // 8N1
  options.c_cflag &= ~CSTOPB;
  options.c_cflag |= CS8;

  options.c_cflag |= CREAD | CLOCAL;          // turn on READ & ignore ctrl lines
  options.c_iflag &= ~(IXON | IXOFF | IXANY); // turn off s/w flow ctrl

  // setup baud rate
  bool custom_baud = false;
  speed_t baud;
  switch (baudrate_)
  {
#ifdef B0
  case 0:
    baud = B0;
    break;
#endif
#ifdef B50
  case 50:
    baud = B50;
    break;
#endif
#ifdef B75
  case 75:
    baud = B75;
    break;
#endif
#ifdef B110
  case 110:
    baud = B110;
    break;
#endif
#ifdef B134
  case 134:
    baud = B134;
    break;
#endif
#ifdef B150
  case 150:
    baud = B150;
    break;
#endif
#ifdef B200
  case 200:
    baud = B200;
    break;
#endif
#ifdef B300
  case 300:
    baud = B300;
    break;
#endif
#ifdef B600
  case 600:
    baud = B600;
    break;
#endif
#ifdef B1200
  case 1200:
    baud = B1200;
    break;
#endif
#ifdef B1800
  case 1800:
    baud = B1800;
    break;
#endif
#ifdef B2400
  case 2400:
    baud = B2400;
    break;
#endif
#ifdef B4800
  case 4800:
    baud = B4800;
    break;
#endif
#ifdef B7200
  case 7200:
    baud = B7200;
    break;
#endif
#ifdef B9600
  case 9600:
    baud = B9600;
    break;
#endif
#ifdef B14400
  case 14400:
    baud = B14400;
    break;
#endif
#ifdef B19200
  case 19200:
    baud = B19200;
    break;
#endif
#ifdef B28800
  case 28800:
    baud = B28800;
    break;
#endif
#ifdef B57600
  case 57600:
    baud = B57600;
    break;
#endif
#ifdef B76800
  case 76800:
    baud = B76800;
    break;
#endif
#ifdef B38400
  case 38400:
    baud = B38400;
    break;
#endif
#ifdef B115200
  case 115200:
    baud = B115200;
    break;
#endif
#ifdef B128000
  case 128000:
    baud = B128000;
    break;
#endif
#ifdef B153600
  case 153600:
    baud = B153600;
    break;
#endif
#ifdef B230400
  case 230400:
    baud = B230400;
    break;
#endif
#ifdef B256000
  case 256000:
    baud = B256000;
    break;
#endif
#ifdef B460800
  case 460800:
    baud = B460800;
    break;
#endif
#ifdef B500000
  case 500000:
    baud = B500000;
    break;
#endif
#ifdef B576000
  case 576000:
    baud = B576000;
    break;
#endif
#ifdef B921600
  case 921600:
    baud = B921600;
    break;
#endif
#ifdef B1000000
  case 1000000:
    baud = B1000000;
    break;
#endif
#ifdef B1152000
  case 1152000:
    baud = B1152000;
    break;
#endif
#ifdef B1500000
  case 1500000:
    baud = B1500000;
    break;
#endif
#ifdef B2000000
  case 2000000:
    baud = B2000000;
    break;
#endif
#ifdef B2500000
  case 2500000:
    baud = B2500000;
    break;
#endif
#ifdef B3000000
  case 3000000:
    baud = B3000000;
    break;
#endif
#ifdef B3500000
  case 3500000:
    baud = B3500000;
    break;
#endif
#ifdef B4000000
  case 4000000:
    baud = B4000000;
    break;
#endif
  default:
    custom_baud = true;
    // OS X support
#if defined(MAC_OS_X_VERSION_10_4) && (MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_4)
    // Starting with Tiger, the IOSSIOSPEED ioctl can be used to set arbitrary baud rates
    // other than those specified by POSIX. The driver for the underlying serial hardware
    // ultimately determines which baud rates can be used. This ioctl sets both the input
    // and output speed.
    speed_t new_baud = static_cast<speed_t>(baudrate_);
    if (-1 == ioctl(fd_, IOSSIOSPEED, &new_baud, 1))
    {
      THROW(IOException, errno);
    }
    // Linux Support
#elif defined(__linux__) && defined(TIOCSSERIAL)
    struct serial_struct ser;

    if (-1 == ioctl(fd_, TIOCGSERIAL, &ser))
    {
      THROW(IOException, errno);
    }

    // set custom divisor
    ser.custom_divisor = ser.baud_base / static_cast<int>(baudrate_);
    // update flags
    ser.flags &= ~ASYNC_SPD_MASK;
    ser.flags |= ASYNC_SPD_CUST;

    if (-1 == ioctl(fd_, TIOCSSERIAL, &ser))
    {
      THROW(IOException, errno);
    }
#else
    throw invalid_argument("OS does not currently support custom bauds");
#endif
  }
  if (custom_baud == false)
  {
#ifdef _BSD_SOURCE
    ::cfsetspeed(&options, baud);
#else
    ::cfsetispeed(&options, baud);
    ::cfsetospeed(&options, baud);
#endif
  }

  // setup char len
  options.c_cflag &= (tcflag_t)~CSIZE;
  if (bytesize_ == eightbits)
    options.c_cflag |= CS8;
  else if (bytesize_ == sevenbits)
    options.c_cflag |= CS7;
  else if (bytesize_ == sixbits)
    options.c_cflag |= CS6;
  else if (bytesize_ == fivebits)
    options.c_cflag |= CS5;
  else
    throw invalid_argument("invalid char len");
  // setup stopbits
  if (stopbits_ == stopbits_one)
    options.c_cflag &= (tcflag_t) ~(CSTOPB);
  else if (stopbits_ == stopbits_one_point_five)
    // ONE POINT FIVE same as TWO.. there is no POSIX support for 1.5
    options.c_cflag |= (CSTOPB);
  else if (stopbits_ == stopbits_two)
    options.c_cflag |= (CSTOPB);
  else
    throw invalid_argument("invalid stop bit");
  // setup parity
  options.c_iflag &= (tcflag_t) ~(INPCK | ISTRIP);
  if (parity_ == parity_none)
  {
    options.c_cflag &= (tcflag_t) ~(PARENB | PARODD);
  }
  else if (parity_ == parity_even)
  {
    options.c_cflag &= (tcflag_t) ~(PARODD);
    options.c_cflag |= (PARENB);
  }
  else if (parity_ == parity_odd)
  {
    options.c_cflag |= (PARENB | PARODD);
  }
#ifdef CMSPAR
  else if (parity_ == parity_mark)
  {
    options.c_cflag |= (PARENB | CMSPAR | PARODD);
  }
  else if (parity_ == parity_space)
  {
    options.c_cflag |= (PARENB | CMSPAR);
    options.c_cflag &= (tcflag_t) ~(PARODD);
  }
#else
  // CMSPAR is not defined on OSX. So do not support mark or space parity.
  else if (parity_ == parity_mark || parity_ == parity_space)
  {
    throw invalid_argument("OS does not support mark or space parity");
  }
#endif // ifdef CMSPAR
  else
  {
    throw invalid_argument("invalid parity");
  }
  // setup flow control
  if (flowcontrol_ == flowcontrol_none)
  {
    xonxoff_ = false;
    rtscts_ = false;
  }
  if (flowcontrol_ == flowcontrol_software)
  {
    xonxoff_ = true;
    rtscts_ = false;
  }
  if (flowcontrol_ == flowcontrol_hardware)
  {
    xonxoff_ = false;
    rtscts_ = true;
  }
  // xonxoff
#ifdef IXANY
  if (xonxoff_)
    options.c_iflag |= (IXON | IXOFF); //|IXANY)
  else
    options.c_iflag &= (tcflag_t) ~(IXON | IXOFF | IXANY);
#else
  if (xonxoff_)
    options.c_iflag |= (IXON | IXOFF);
  else
    options.c_iflag &= (tcflag_t) ~(IXON | IXOFF);
#endif
    // rtscts
#ifdef CRTSCTS
  if (rtscts_)
    options.c_cflag |= (CRTSCTS);
  else
    options.c_cflag &= (unsigned long)~(CRTSCTS);
#elif defined CNEW_RTSCTS
  if (rtscts_)
    options.c_cflag |= (CNEW_RTSCTS);
  else
    options.c_cflag &= (unsigned long)~(CNEW_RTSCTS);
#else
#error "OS Support seems wrong."
#endif

  // http://www.unixwiz.net/techtips/termios-vmin-vtime.html
  // this basically sets the read call up to be a polling read,
  // but we are using select to ensure there is data available
  // to read before each call, so we should never needlessly poll
  options.c_cc[VMIN] = 0;                    // Does not work well with VTIME in some drivers
  options.c_cc[VTIME] = (realtime_ ? 0 : 1); // 100 ms to receive at least one byte

  // activate settings
  ::tcsetattr(fd_, TCSANOW, &options);

  // Update byte_time_ based on the new settings.
  uint32_t bit_time_ns = 1e9 / baudrate_;
  byte_time_ns_ = bit_time_ns * (1 + bytesize_ + parity_ + stopbits_);

  // Compensate for the stopbits_one_point_five enum being equal to int 3,
  // and not 1.5.
  if (stopbits_ == stopbits_one_point_five)
  {
    byte_time_ns_ += ((1.5 - (double)stopbits_one_point_five) * bit_time_ns);
  }
}

void Serial::SerialImpl::close()
{

  if (is_open_ == true)
  {
    is_open_ = false;
    if (fd_ != -1)
    {
      int ret;
      ret = ::close(fd_);
      if (ret == 0)
      {
        fd_ = -1;
      }
      else
      {
        THROW(IOException, errno);
      }
    }
  }
}

bool Serial::SerialImpl::isOpen() const
{
  if(this != nullptr)
    return is_open_;
  else
    return false;
}

size_t
Serial::SerialImpl::available()
{
  if (!is_open_)
  {
    return 0;
  }
  int count = 0;
  if (-1 == ioctl(fd_, TIOCINQ, &count))
  {
    // THROW(IOException, errno);
    return 0;
  }
  else
  {
    return static_cast<size_t>(count);
  }
}

bool Serial::SerialImpl::waitReadable(uint32_t timeout)
{
  // Setup a select call to block for serial data or a timeout
  fd_set readfds;
  struct pollfd fds[1];
  fds[0].events = POLLIN;
  fds[0].fd = fd_;

  int r = poll(fds, 1, timeout);

  if (r < 0)
  {
    // Select was interrupted
    if (errno == EINTR)
    {
      return false;
    }
    // Otherwise there was some error
    THROW(IOException, errno);
  }
  // Timeout occurred
  if (r == 0)
  {
    return false;
  }
  // This shouldn't happen, if r > 0 our fd has to be in the list!
  if (!FD_ISSET(fd_, &readfds))
  {
    this->close();
    THROW(IOException, "select reports ready to read, but our fd isn't"
                       " in the list, this shouldn't happen!");
  }
  // Data available to read.
  return true;
}

void Serial::SerialImpl::waitByteTimes(size_t count)
{
  timespec wait_time = {0, static_cast<long>(byte_time_ns_ * count)};
  pselect(0, NULL, NULL, NULL, &wait_time, NULL);
}

size_t
Serial::SerialImpl::read(uint8_t *buf, size_t size)
{
  // If the port is not open, throw
  if (!this)
    return 0;

  if (!is_open_)
  {
    this->close();
    throw PortNotOpenedException("Serial::read");
  }
  size_t bytes_read = 0;

  // Calculate total timeout in milliseconds t_c + (t_m * N)
  long total_timeout_ms = timeout_.read_timeout_constant;
  total_timeout_ms += timeout_.read_timeout_multiplier * static_cast<long>(size);
  MillisecondTimer total_timeout(total_timeout_ms);

  // Pre-fill buffer with available bytes
  {
    // int len;
    // ioctl(fd_, FIONREAD, &len);

    // printf("%d:%d\n", len, xxx);
    // profiling_timer_start(9);
    ssize_t bytes_read_now = ::read(fd_, buf, size);
    if (bytes_read_now > 0)
    {
      bytes_read = bytes_read_now;
    }
  }

  while (bytes_read < size)
  {
    int64_t timeout_remaining_ms = total_timeout.remaining();
    if (timeout_remaining_ms <= 0)
    {
      // printf("Timed out\n");
      // Timed out
      break;
    }
    // Timeout for the next read is whichever is less of the remaining
    // total read timeout
    uint32_t timeout = static_cast<uint32_t>(timeout_remaining_ms);

    // printf("timeout:%d\n", timeout);

    ssize_t bytes_read_now =
        ::read(fd_, buf + bytes_read, size - bytes_read);
    // Update bytes_read
    bytes_read += static_cast<size_t>(bytes_read_now);
    // If bytes_read == size then we have read everything we need
    if (bytes_read == size)
    {
      // printf("bytes_read == size\n");
      break;
    }
    // If bytes_read < size then we have more to read
    if (bytes_read < size)
    {
      if (realtime_)
      {
        // profiling_timer_start(9);
        // usleep(byte_time_ns_ / 1000);
        usleep(10);
        // uint32_t dd = profiling_timer_end(9) / 1000;
        // printf("usleep=%d, baaud=%d\n", dd, byte_time_ns_ / 1000);
      }

      continue;
    }
    // If bytes_read > size then we have over read, which shouldn't happen
    if (bytes_read > size)
    {
      throw SerialException("read over read, too many bytes where "
                            "read, this shouldn't happen, might be "
                            "a logical error!");
    }
    // }
  }

  // Check disconnection
  int null_check;
  ioctl(fd_, FIONREAD, &null_check);
  if (errno == 5)
  {
    this->close();
    throw PortNotOpenedException("Serial::Read");
  }

  return bytes_read;
}

size_t
Serial::SerialImpl::write(const uint8_t *data, size_t length)
{
  if (is_open_ == false)
  {
    // throw PortNotOpenedException("Serial::write");
    return length;
  }
  fd_set writefds;
  size_t bytes_written = 0;

  // Calculate total timeout in milliseconds t_c + (t_m * N)
  long total_timeout_ms = timeout_.write_timeout_constant;
  total_timeout_ms += timeout_.write_timeout_multiplier * static_cast<long>(length);
  MillisecondTimer total_timeout(total_timeout_ms);

  bool first_iteration = true;
  while (bytes_written < length)
  {
    int64_t timeout_remaining_ms = total_timeout.remaining();
    // Only consider the timeout if it's not the first iteration of the loop
    // otherwise a timeout of 0 won't be allowed through
    if (!first_iteration && (timeout_remaining_ms <= 0))
    {
      // Timed out
      break;
    }
    first_iteration = false;

    timespec timeout(timespec_from_ms(timeout_remaining_ms));

    FD_ZERO(&writefds);
    FD_SET(fd_, &writefds);

    // Do the select
    int r = pselect(fd_ + 1, NULL, &writefds, NULL, &timeout, NULL);

    // Figure out what happened by looking at select's response 'r'
    /** Error **/
    if (r < 0)
    {
      // Select was interrupted, try again
      if (errno == EINTR)
      {
        continue;
      }
      // Otherwise there was some error
      // THROW(IOException, errno);
      return 0;
    }
    /** Timeout **/
    if (r == 0)
    {
      break;
    }
    /** Port ready to write **/
    if (r > 0)
    {
      // Make sure our file descriptor is in the ready to write list
      if (FD_ISSET(fd_, &writefds))
      {
        // This will write some
        ssize_t bytes_written_now =
            ::write(fd_, data + bytes_written, length - bytes_written);
        // write should always return some data as select reported it was
        // ready to write when we get to this point.
        if (bytes_written_now < 1)
        {
          // Disconnected devices, at least on Linux, show the
          // behavior that they are always ready to write immediately
          // but writing returns nothing.
          throw SerialException("device reports readiness to write but "
                                "returned no data (device disconnected?)");
        }
        // Update bytes_written
        bytes_written += static_cast<size_t>(bytes_written_now);
        // If bytes_written == size then we have written everything we need to
        if (bytes_written == length)
        {
          break;
        }
        // If bytes_written < size then we have more to write
        if (bytes_written < length)
        {
          continue;
        }
        // If bytes_written > size then we have over written, which shouldn't happen
        if (bytes_written > length)
        {
          throw SerialException("write over wrote, too many bytes where "
                                "written, this shouldn't happen, might be "
                                "a logical error!");
        }
      }
      // This shouldn't happen, if r > 0 our fd has to be in the list!
      THROW(IOException, "select reports ready to write, but our fd isn't"
                         " in the list, this shouldn't happen!");
    }
  }
  return bytes_written;
}

void Serial::SerialImpl::setPort(const string &port)
{
  port_ = port;
}

string
Serial::SerialImpl::getPort() const
{
  return port_;
}

void Serial::SerialImpl::setTimeout(serial::Timeout &timeout)
{
  timeout_ = timeout;
}

serial::Timeout
Serial::SerialImpl::getTimeout() const
{
  return timeout_;
}

void Serial::SerialImpl::setBaudrate(unsigned long baudrate)
{
  baudrate_ = baudrate;
  if (is_open_)
    reconfigurePort();
}

unsigned long
Serial::SerialImpl::getBaudrate() const
{
  return baudrate_;
}

void Serial::SerialImpl::setBytesize(serial::bytesize_t bytesize)
{
  bytesize_ = bytesize;
  if (is_open_)
    reconfigurePort();
}

serial::bytesize_t
Serial::SerialImpl::getBytesize() const
{
  return bytesize_;
}

void Serial::SerialImpl::setParity(serial::parity_t parity)
{
  parity_ = parity;
  if (is_open_)
    reconfigurePort();
}

serial::parity_t
Serial::SerialImpl::getParity() const
{
  return parity_;
}

void Serial::SerialImpl::setStopbits(serial::stopbits_t stopbits)
{
  stopbits_ = stopbits;
  if (is_open_)
    reconfigurePort();
}

serial::stopbits_t
Serial::SerialImpl::getStopbits() const
{
  return stopbits_;
}

void Serial::SerialImpl::setFlowcontrol(serial::flowcontrol_t flowcontrol)
{
  flowcontrol_ = flowcontrol;
  if (is_open_)
    reconfigurePort();
}

serial::flowcontrol_t
Serial::SerialImpl::getFlowcontrol() const
{
  return flowcontrol_;
}

void Serial::SerialImpl::flush()
{
  if (is_open_ == false)
  {
    throw PortNotOpenedException("Serial::flush");
  }
  tcdrain(fd_);
}

void Serial::SerialImpl::flushInput()
{
  if (is_open_ == false)
  {
    throw PortNotOpenedException("Serial::flushInput");
  }
  tcflush(fd_, TCIFLUSH);
}

void Serial::SerialImpl::flushOutput()
{
  if (is_open_ == false)
  {
    throw PortNotOpenedException("Serial::flushOutput");
  }
  tcflush(fd_, TCOFLUSH);
}

void Serial::SerialImpl::sendBreak(int duration)
{
  if (is_open_ == false)
  {
    throw PortNotOpenedException("Serial::sendBreak");
  }
  tcsendbreak(fd_, static_cast<int>(duration / 4));
}

void Serial::SerialImpl::setBreak(bool level)
{
  if (is_open_ == false)
  {
    throw PortNotOpenedException("Serial::setBreak");
  }

  if (level)
  {
    if (-1 == ioctl(fd_, TIOCSBRK))
    {
      stringstream ss;
      ss << "setBreak failed on a call to ioctl(TIOCSBRK): " << errno << " " << strerror(errno);
      throw(SerialException(ss.str().c_str()));
    }
  }
  else
  {
    if (-1 == ioctl(fd_, TIOCCBRK))
    {
      stringstream ss;
      ss << "setBreak failed on a call to ioctl(TIOCCBRK): " << errno << " " << strerror(errno);
      throw(SerialException(ss.str().c_str()));
    }
  }
}

void Serial::SerialImpl::setRTS(bool level)
{
  if (is_open_ == false)
  {
    throw PortNotOpenedException("Serial::setRTS");
  }

  int command = TIOCM_RTS;

  if (level)
  {
    if (-1 == ioctl(fd_, TIOCMBIS, &command))
    {
      stringstream ss;
      ss << "setRTS failed on a call to ioctl(TIOCMBIS): " << errno << " " << strerror(errno);
      throw(SerialException(ss.str().c_str()));
    }
  }
  else
  {
    if (-1 == ioctl(fd_, TIOCMBIC, &command))
    {
      stringstream ss;
      ss << "setRTS failed on a call to ioctl(TIOCMBIC): " << errno << " " << strerror(errno);
      throw(SerialException(ss.str().c_str()));
    }
  }
}

void Serial::SerialImpl::setDTR(bool level)
{
  if (is_open_ == false)
  {
    throw PortNotOpenedException("Serial::setDTR");
  }

  int command = TIOCM_DTR;

  if (level)
  {
    if (-1 == ioctl(fd_, TIOCMBIS, &command))
    {
      stringstream ss;
      ss << "setDTR failed on a call to ioctl(TIOCMBIS): " << errno << " " << strerror(errno);
      throw(SerialException(ss.str().c_str()));
    }
  }
  else
  {
    if (-1 == ioctl(fd_, TIOCMBIC, &command))
    {
      stringstream ss;
      ss << "setDTR failed on a call to ioctl(TIOCMBIC): " << errno << " " << strerror(errno);
      throw(SerialException(ss.str().c_str()));
    }
  }
}

bool Serial::SerialImpl::waitForChange()
{
#ifndef TIOCMIWAIT

  while (is_open_ == true)
  {

    int status;

    if (-1 == ioctl(fd_, TIOCMGET, &status))
    {
      stringstream ss;
      ss << "waitForChange failed on a call to ioctl(TIOCMGET): " << errno << " " << strerror(errno);
      throw(SerialException(ss.str().c_str()));
    }
    else
    {
      if (0 != (status & TIOCM_CTS) || 0 != (status & TIOCM_DSR) || 0 != (status & TIOCM_RI) || 0 != (status & TIOCM_CD))
      {
        return true;
      }
    }

    usleep(1000);
  }

  return false;
#else
  int command = (TIOCM_CD | TIOCM_DSR | TIOCM_RI | TIOCM_CTS);

  if (-1 == ioctl(fd_, TIOCMIWAIT, &command))
  {
    stringstream ss;
    ss << "waitForDSR failed on a call to ioctl(TIOCMIWAIT): "
       << errno << " " << strerror(errno);
    throw(SerialException(ss.str().c_str()));
  }
  return true;
#endif
}

bool Serial::SerialImpl::getCTS()
{
  if (is_open_ == false)
  {
    throw PortNotOpenedException("Serial::getCTS");
  }

  int status;

  if (-1 == ioctl(fd_, TIOCMGET, &status))
  {
    stringstream ss;
    ss << "getCTS failed on a call to ioctl(TIOCMGET): " << errno << " " << strerror(errno);
    throw(SerialException(ss.str().c_str()));
  }
  else
  {
    return 0 != (status & TIOCM_CTS);
  }
}

bool Serial::SerialImpl::getDSR()
{
  if (is_open_ == false)
  {
    throw PortNotOpenedException("Serial::getDSR");
  }

  int status;

  if (-1 == ioctl(fd_, TIOCMGET, &status))
  {
    stringstream ss;
    ss << "getDSR failed on a call to ioctl(TIOCMGET): " << errno << " " << strerror(errno);
    throw(SerialException(ss.str().c_str()));
  }
  else
  {
    return 0 != (status & TIOCM_DSR);
  }
}

bool Serial::SerialImpl::getRI()
{
  if (is_open_ == false)
  {
    throw PortNotOpenedException("Serial::getRI");
  }

  int status;

  if (-1 == ioctl(fd_, TIOCMGET, &status))
  {
    stringstream ss;
    ss << "getRI failed on a call to ioctl(TIOCMGET): " << errno << " " << strerror(errno);
    throw(SerialException(ss.str().c_str()));
  }
  else
  {
    return 0 != (status & TIOCM_RI);
  }
}

bool Serial::SerialImpl::getCD()
{
  if (is_open_ == false)
  {
    throw PortNotOpenedException("Serial::getCD");
  }

  int status;

  if (-1 == ioctl(fd_, TIOCMGET, &status))
  {
    stringstream ss;
    ss << "getCD failed on a call to ioctl(TIOCMGET): " << errno << " " << strerror(errno);
    throw(SerialException(ss.str().c_str()));
  }
  else
  {
    return 0 != (status & TIOCM_CD);
  }
}

void Serial::SerialImpl::readLock()
{
  int result = pthread_mutex_lock(&this->read_mutex);
  if (result)
  {
    THROW(IOException, result);
  }
}

void Serial::SerialImpl::readUnlock()
{
  int result = pthread_mutex_unlock(&this->read_mutex);
  if (result)
  {
    THROW(IOException, result);
  }
}

void Serial::SerialImpl::writeLock()
{
  int result = pthread_mutex_lock(&this->write_mutex);
  if (result)
  {
    THROW(IOException, result);
  }
}

void Serial::SerialImpl::writeUnlock()
{
  int result = pthread_mutex_unlock(&this->write_mutex);
  if (result)
  {
    THROW(IOException, result);
  }
}

#endif // !defined(_WIN32)
